// expose private and protected members for invasive testing
#if openPMD_USE_INVASIVE_TESTS
#define OPENPMD_private public:
#define OPENPMD_protected public:
#endif

#include "openPMD/ChunkInfo.hpp"
#include "openPMD/openPMD.hpp"

#include "Files_Core/CoreTests.hpp"

#include "openPMD/IO/ADIOS/macros.hpp"
#include "openPMD/auxiliary/Filesystem.hpp"
#include "openPMD/auxiliary/JSON.hpp"
#include "openPMD/auxiliary/UniquePtr.hpp"

#include <catch2/catch.hpp>

#include <algorithm>
#include <array>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <sstream>
// cstdlib does not have setenv
#include <stdlib.h> // NOLINT(modernize-deprecated-headers)
#include <string>
#include <vector>

// On Windows, REQUIRE() might not be able to print more complex data structures
// upon failure:
// CoreTest.obj : error LNK2001: unresolved external symbol
// "class std::string const Catch::Detail::unprintableString" (...)
#ifdef _WIN32
#define OPENPMD_REQUIRE_GUARD_WINDOWS(...)                                     \
    do                                                                         \
    {                                                                          \
        bool guarded_require_boolean = __VA_ARGS__;                            \
        REQUIRE(guarded_require_boolean);                                      \
    } while (0);
#else
#define OPENPMD_REQUIRE_GUARD_WINDOWS(...) REQUIRE(__VA_ARGS__)
#endif

using namespace openPMD;

Dataset globalDataset(Datatype::CHAR, {1});

namespace test_chunk_assignment
{
using namespace openPMD::chunk_assignment;
struct Params
{
    ChunkTable table;
    RankMeta metaSource;
    RankMeta metaSink;

    void init(
        size_t sourceRanks,
        size_t sinkRanks,
        size_t in_per_host,
        size_t out_per_host)
    {
        for (size_t rank = 0; rank < sourceRanks; ++rank)
        {
            table.emplace_back(Offset{rank, rank}, Extent{rank, rank}, rank);
            table.emplace_back(
                Offset{rank, 100 * rank}, Extent{rank, 100 * rank}, rank);
            metaSource.emplace(rank, std::to_string(rank / in_per_host));
        }
        for (size_t rank = 0; rank < sinkRanks; ++rank)
        {
            metaSink.emplace(rank, std::to_string(rank / out_per_host));
        }
    }
};

static constexpr bool verbose = false;
void print(RankMeta const &meta, ChunkTable const &table)
{
    if (!verbose)
    {
        return;
    }
    for (auto const &chunk : table)
    {
        std::cout << "[HOST: " << meta.at(chunk.sourceID)
                  << ",\tRank: " << chunk.sourceID << ",\tOffset: ";
        for (auto offset : chunk.offset)
        {
            std::cout << offset << ", ";
        }
        std::cout << "\tExtent: ";
        for (auto extent : chunk.extent)
        {
            std::cout << extent << ", ";
        }
        std::cout << "]" << std::endl;
    }
}
void print(RankMeta const &meta, Assignment const &table)
{
    if (!verbose)
    {
        return;
    }
    for (auto &[rank, chunkList] : table)
    {
        std::cout << "[HOST: " << meta.at(rank) << ",\tRank: " << rank << "]"
                  << std::endl;
        for (auto const &chunk : chunkList)
        {
            std::cout << "\t[From " << chunk.sourceID << "\tOffset: ";
            for (auto offset : chunk.offset)
            {
                std::cout << offset << ", ";
            }
            std::cout << "\tExtent: ";
            for (auto extent : chunk.extent)
            {
                std::cout << extent << ", ";
            }
            std::cout << "]" << std::endl;
        }
    }
}

static auto add = [](size_t left, size_t right) { return left + right; };
auto mergeTable(ChunkTable const &chunkTable) -> ChunkTable const &
{
    return chunkTable;
}
auto mergeTable(chunk_assignment::Assignment const &assignment) -> ChunkTable
{
    ChunkTable merged;
    merged.reserve(
        std::transform_reduce(
            assignment.begin(),
            assignment.end(),
            0u,
            add,
            [](chunk_assignment::Assignment::value_type const &pair) {
                return pair.second.size();
            }));
    for (auto const &pair : assignment)
    {
        for (auto const &chunk : pair.second)
        {
            merged.insert(merged.end(), chunk);
        }
    }
    return merged;
}
auto mergeTable(chunk_assignment::PartialAssignment const &assignment)
{
    auto const &[not_assigned, assigned] = assignment;
    ChunkTable merged = mergeTable(assigned);
    merged.reserve(merged.size() + not_assigned.size());
    for (auto const &chunk : not_assigned)
    {
        merged.insert(merged.end(), chunk);
    }
    return merged;
}

template <typename ChunkTable1, typename ChunkTable2>
auto equalTables(ChunkTable1 &&availableChunks, ChunkTable2 &&assignedChunks)
{
    return chunk_assignment::mergeChunksFromSameSourceID(
               mergeTable(availableChunks)) ==
        chunk_assignment::mergeChunksFromSameSourceID(
               mergeTable(assignedChunks));
}

void verifyHostnameAssignment(
    chunk_assignment::PartialAssignment const &assignment,
    chunk_assignment::RankMeta const &in,
    chunk_assignment::RankMeta const &out)
{
    REQUIRE(!assignment.assigned.empty());
    for (auto const &[out_rank, chunks] : assignment.assigned)
    {
        for (auto const &chunk : chunks)
        {
            REQUIRE(in.at(chunk.sourceID) == out.at(out_rank));
        }
    }
    for (auto const &chunk : assignment.notAssigned)
    {
        auto const &hostname = in.at(chunk.sourceID);
        OPENPMD_REQUIRE_GUARD_WINDOWS(
            std::none_of(
                out.begin(),
                out.end(),
                [&hostname](
                    chunk_assignment::RankMeta::value_type const &pair) {
                    return pair.second == hostname;
                }));
    }
}
} // namespace test_chunk_assignment

TEST_CASE("chunk_assignment", "[core]")
{
    using namespace chunk_assignment;
    test_chunk_assignment::Params params;
    params.init(6, 2, 2, 1);
    test_chunk_assignment::print(params.metaSource, params.table);
    ByHostname byHostname(std::make_unique<RoundRobin>());

    PartialAssignment partial_res0 = byHostname.assign(
        params.table, params.metaSource, params.metaSink, 0, 2);
    PartialAssignment partial_res1 = byHostname.assign(
        params.table, params.metaSource, params.metaSink, 0, 2);

    OPENPMD_REQUIRE_GUARD_WINDOWS(
        partial_res0.notAssigned == partial_res1.notAssigned);
    PartialAssignment partial_res{
        partial_res0.notAssigned,
        {{0, partial_res0.assigned[0]}, {1, partial_res1.assigned[1]}}};
    test_chunk_assignment::verifyHostnameAssignment(
        partial_res, params.metaSource, params.metaSink);

    FromPartialStrategy fullStrategy(
        std::make_unique<ByHostname>(std::move(byHostname)),
        std::make_unique<BinPacking>());
    Assignment res0 = fullStrategy.assign(
        params.table, params.metaSource, params.metaSink, 0, 2);
    Assignment res1 = fullStrategy.assign(
        params.table, params.metaSource, params.metaSink, 1, 2);
    Assignment res = {{0, res0[0]}, {1, res1[1]}};

    OPENPMD_REQUIRE_GUARD_WINDOWS(
        test_chunk_assignment::equalTables(params.table, res));

    test_chunk_assignment::print(params.metaSink, res1);
}

TEST_CASE("versions_test", "[core]")
{
    auto const apiVersion = getVersion();
    auto const is_dot = [](char const c) { return c == '.'; };
    REQUIRE(2u == std::count_if(apiVersion.begin(), apiVersion.end(), is_dot));

    auto const standardDefault = getStandardDefault();
    REQUIRE(standardDefault == "1.1.0");

    auto const standard = getStandardMaximum();
    REQUIRE(standard == "2.0.0");

    auto const standardMin = getStandardMinimum();
    REQUIRE(standardMin == "1.0.0");

    auto const featureVariants = getVariants();
    REQUIRE(featureVariants.at("json") == true);
}

TEST_CASE("attribute_dtype_test", "[core]")
{
    Attribute a = Attribute(static_cast<char>(' '));
    REQUIRE(Datatype::CHAR == a.dtype);
    {
        // check that copy constructor works
        // NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
        [[maybe_unused]] Attribute b = a;
    }
    {
        // check that move constructor works
        [[maybe_unused]] Attribute b = std::move(a);
    }
    a = Attribute(static_cast<unsigned char>(' '));
    REQUIRE(Datatype::UCHAR == a.dtype);
    a = Attribute(static_cast<short>(0));
    REQUIRE(Datatype::SHORT == a.dtype);
    a = Attribute(static_cast<int>(0));
    REQUIRE(Datatype::INT == a.dtype);
    a = Attribute(static_cast<long>(0));
    REQUIRE(Datatype::LONG == a.dtype);
    a = Attribute(static_cast<long long>(0));
    REQUIRE(Datatype::LONGLONG == a.dtype);
    a = Attribute(static_cast<unsigned short>(0));
    REQUIRE(Datatype::USHORT == a.dtype);
    a = Attribute(static_cast<unsigned int>(0));
    REQUIRE(Datatype::UINT == a.dtype);
    a = Attribute(static_cast<unsigned long>(0));
    REQUIRE(Datatype::ULONG == a.dtype);
    a = Attribute(static_cast<unsigned long long>(0));
    REQUIRE(Datatype::ULONGLONG == a.dtype);
    a = Attribute(static_cast<float>(0.));
    REQUIRE(Datatype::FLOAT == a.dtype);
    a = Attribute(static_cast<double>(0.));
    REQUIRE(Datatype::DOUBLE == a.dtype);
    a = Attribute(static_cast<long double>(0.));
    REQUIRE(Datatype::LONG_DOUBLE == a.dtype);
    a = Attribute(static_cast<std::complex<float>>(0.));
    REQUIRE(Datatype::CFLOAT == a.dtype);
    a = Attribute(static_cast<std::complex<double>>(0.));
    REQUIRE(Datatype::CDOUBLE == a.dtype);
    a = Attribute(static_cast<std::complex<long double>>(0.));
    REQUIRE(Datatype::CLONG_DOUBLE == a.dtype);
    a = Attribute(std::string(""));
    REQUIRE(Datatype::STRING == a.dtype);
    a = Attribute(std::vector<char>());
    REQUIRE(Datatype::VEC_CHAR == a.dtype);
    a = Attribute(std::vector<short>());
    REQUIRE(Datatype::VEC_SHORT == a.dtype);
    a = Attribute(std::vector<int>());
    REQUIRE(Datatype::VEC_INT == a.dtype);
    a = Attribute(std::vector<long>());
    REQUIRE(Datatype::VEC_LONG == a.dtype);
    a = Attribute(std::vector<long long>());
    REQUIRE(Datatype::VEC_LONGLONG == a.dtype);
    a = Attribute(std::vector<unsigned char>());
    REQUIRE(Datatype::VEC_UCHAR == a.dtype);
    a = Attribute(std::vector<unsigned short>());
    REQUIRE(Datatype::VEC_USHORT == a.dtype);
    a = Attribute(std::vector<unsigned int>());
    REQUIRE(Datatype::VEC_UINT == a.dtype);
    a = Attribute(std::vector<unsigned long>());
    REQUIRE(Datatype::VEC_ULONG == a.dtype);
    a = Attribute(std::vector<unsigned long long>());
    REQUIRE(Datatype::VEC_ULONGLONG == a.dtype);
    a = Attribute(std::vector<float>());
    REQUIRE(Datatype::VEC_FLOAT == a.dtype);
    a = Attribute(std::vector<double>());
    REQUIRE(Datatype::VEC_DOUBLE == a.dtype);
    a = Attribute(std::vector<long double>());
    REQUIRE(Datatype::VEC_LONG_DOUBLE == a.dtype);
    a = Attribute(std::vector<std::string>());
    REQUIRE(Datatype::VEC_STRING == a.dtype);
    a = Attribute(std::array<double, 7>());
    REQUIRE(Datatype::ARR_DBL_7 == a.dtype);
    a = Attribute(static_cast<bool>(false));
    REQUIRE(Datatype::BOOL == a.dtype);

    // fixed size integers
    a = Attribute(static_cast<int16_t>(0));
    REQUIRE(determineDatatype<int16_t>() == a.dtype);
    a = Attribute(static_cast<int32_t>(0));
    REQUIRE(determineDatatype<int32_t>() == a.dtype);
    a = Attribute(static_cast<int64_t>(0));
    REQUIRE(determineDatatype<int64_t>() == a.dtype);
    a = Attribute(static_cast<uint16_t>(0));
    REQUIRE(determineDatatype<uint16_t>() == a.dtype);
    a = Attribute(static_cast<uint32_t>(0));
    REQUIRE(determineDatatype<uint32_t>() == a.dtype);
    a = Attribute(static_cast<uint64_t>(0));
    REQUIRE(determineDatatype<uint64_t>() == a.dtype);
    // TODO fixed size floats

    // same implementation types (not necessary aliases) detection
    if (sizeof(long) == sizeof(long long))
    {
        a = Attribute(static_cast<long>(0));
        REQUIRE(isSame(Datatype::LONGLONG, a.dtype));
#if !defined(_MSC_VER)
        REQUIRE(Datatype::LONGLONG == a.dtype);
#endif
        a = Attribute(static_cast<long long>(0));
        REQUIRE(isSame(Datatype::LONG, a.dtype));
#if !defined(_MSC_VER)
        REQUIRE(Datatype::LONG == a.dtype);
#endif
    }
    if (sizeof(int) == sizeof(long))
    {
        a = Attribute(static_cast<long>(0));
        REQUIRE(isSame(Datatype::INT, a.dtype));
#if !defined(_MSC_VER)
        REQUIRE(Datatype::INT == a.dtype);
#endif
        a = Attribute(static_cast<int>(0));
        REQUIRE(isSame(Datatype::LONG, a.dtype));
#if !defined(_MSC_VER)
        REQUIRE(Datatype::LONG == a.dtype);
#endif
    }
}

TEST_CASE("myPath", "[core]")
{
#if openPMD_USE_INVASIVE_TESTS
    using vec_t = std::vector<std::string>;
    auto pathOf = [](Attributable const &attr) {
        auto res = attr.myPath();
#if false
        std::cout << "Directory:\t" << res.directory << "\nSeries name:\t"
                  << res.seriesName << "\nSeries ext:\t" << res.seriesExtension
                  << std::endl;
#endif
        REQUIRE(res.directory == "../samples/");
        REQUIRE(res.seriesName == "myPath");
        REQUIRE(res.seriesExtension == ".json");
        REQUIRE(res.filePath() == "../samples/myPath.json");
        return res.group;
    };

    Series series("../samples/myPath.json", Access::CREATE);
    REQUIRE(pathOf(series) == vec_t{});
    REQUIRE(pathOf(series.iterations) == vec_t{"data"});
    REQUIRE(pathOf(series.snapshots()) == vec_t{"data"});
    auto iteration = series.iterations[1234];
    REQUIRE(pathOf(iteration) == vec_t{"data", "1234"});

    auto writeSomething = [](auto &recordComponent) {
        recordComponent.resetDataset({Datatype::INT, {100}});
        recordComponent.template makeConstant<int>(5678);
    };

    REQUIRE(pathOf(iteration.meshes) == vec_t{"data", "1234", "meshes"});

    auto scalarMesh = iteration.meshes["e_chargeDensity"];
    REQUIRE(
        pathOf(scalarMesh) ==
        vec_t{"data", "1234", "meshes", "e_chargeDensity"});
    auto scalarMeshComponent = scalarMesh[RecordComponent::SCALAR].resetDataset(
        {Datatype::FLOAT, {10}});
    REQUIRE(
        pathOf(scalarMeshComponent) ==
        vec_t{"data", "1234", "meshes", "e_chargeDensity"});
    writeSomething(scalarMeshComponent);

    auto vectorMesh = iteration.meshes["E"];
    REQUIRE(pathOf(vectorMesh) == vec_t{"data", "1234", "meshes", "E"});
    auto vectorMeshComponent =
        vectorMesh["x"].resetDataset({Datatype::FLOAT, {10}});
    REQUIRE(
        pathOf(vectorMeshComponent) ==
        vec_t{"data", "1234", "meshes", "E", "x"});

    REQUIRE(pathOf(iteration.particles) == vec_t{"data", "1234", "particles"});

    auto speciesE = iteration.particles["e"];
    REQUIRE(pathOf(speciesE) == vec_t{"data", "1234", "particles", "e"});

    auto speciesPosition = speciesE["position"];
    REQUIRE(
        pathOf(speciesPosition) ==
        vec_t{"data", "1234", "particles", "e", "position"});

    auto speciesPositionX =
        speciesPosition["x"].resetDataset({Datatype::FLOAT, {10}});
    REQUIRE(
        pathOf(speciesPositionX) ==
        vec_t{"data", "1234", "particles", "e", "position", "x"});
    writeSomething(speciesPositionX);

    auto speciesWeighting = speciesE["weighting"];
    REQUIRE(
        pathOf(speciesWeighting) ==
        vec_t{"data", "1234", "particles", "e", "weighting"});

    auto speciesWeightingX =
        speciesWeighting[RecordComponent::SCALAR].resetDataset(
            {Datatype::FLOAT, {10}});
    REQUIRE(
        pathOf(speciesWeightingX) ==
        vec_t{"data", "1234", "particles", "e", "weighting"});
    writeSomething(speciesWeightingX);

    REQUIRE(
        pathOf(speciesE.particlePatches) ==
        vec_t{"data", "1234", "particles", "e", "particlePatches"});

    auto patchExtent = speciesE.particlePatches["extent"];
    REQUIRE(
        pathOf(patchExtent) ==
        vec_t{"data", "1234", "particles", "e", "particlePatches", "extent"});

    auto patchExtentX = patchExtent["x"].resetDataset({Datatype::INT, {10}});
    REQUIRE(
        pathOf(patchExtentX) ==
        vec_t{
            "data",
            "1234",
            "particles",
            "e",
            "particlePatches",
            "extent",
            "x"});

    auto patchNumParticles = speciesE.particlePatches["numParticles"];
    REQUIRE(
        pathOf(patchNumParticles) ==
        vec_t{
            "data",
            "1234",
            "particles",
            "e",
            "particlePatches",
            "numParticles"});

    auto patchNumParticlesComponent =
        patchNumParticles[RecordComponent::SCALAR].resetDataset(
            {Datatype::INT, {10}});
    REQUIRE(
        pathOf(patchNumParticlesComponent) ==
        vec_t{
            "data",
            "1234",
            "particles",
            "e",
            "particlePatches",
            "numParticles"});

    speciesE.particlePatches["offset"]["x"].resetDataset({Datatype::INT, {10}});
    speciesE.particlePatches["numParticlesOffset"][RecordComponent::SCALAR]
        .resetDataset({Datatype::INT, {10}});
#endif
}

TEST_CASE("output_default_test", "[core]")
{
    using IE = IterationEncoding;
    Series o = Series("./new_openpmd_output_%T.json", Access::CREATE);

    REQUIRE(o.openPMD() == "1.1.0");
    REQUIRE(o.openPMDextension() == static_cast<uint32_t>(0));
    REQUIRE(o.basePath() == "/data/%T/");
    REQUIRE(o.iterationEncoding() == IE::fileBased);
    REQUIRE(o.iterationFormat() == "new_openpmd_output_%T");
    REQUIRE(o.iterations.empty());
    REQUIRE(
        o.numAttributes() ==
        8); /* openPMD, openPMDextension, basePath, iterationEncoding,
               iterationFormat, date, software, softwareVersion */
    REQUIRE(o.name() == "new_openpmd_output_%T");

    o.iterations[0];
}

TEST_CASE("output_constructor_test", "[core]")
{
    using IE = IterationEncoding;
    Series o = Series("./MyCustomOutput.json", Access::CREATE);

    o.setMeshesPath("customMeshesPath").setParticlesPath("customParticlesPath");

    o.iterations[1].meshes["foo"]["baz"].resetDataset(
        Dataset(Datatype::DOUBLE, {1}));
    auto species = o.iterations[1].particles["bar"];
    auto dset = Dataset(Datatype::DOUBLE, {1});
    species["position"][RecordComponent::SCALAR].resetDataset(dset);
    species["positionOffset"][RecordComponent::SCALAR].resetDataset(dset);

    REQUIRE(o.openPMD() == "1.1.0");
    REQUIRE(o.openPMDextension() == static_cast<uint32_t>(0));
    REQUIRE(o.basePath() == "/data/%T/");
    REQUIRE(o.meshesPath() == "customMeshesPath/");
    REQUIRE(o.particlesPath() == "customParticlesPath/");
    REQUIRE(o.iterationEncoding() == IE::groupBased);
    REQUIRE(o.iterationFormat() == "/data/%T/");
    REQUIRE(o.iterations.size() == 1);
    REQUIRE(
        o.numAttributes() ==
        10); /* openPMD, openPMDextension, basePath, meshesPath, particlesPath,
                iterationEncoding, iterationFormat, date, software,
                softwareVersion */
    REQUIRE(o.name() == "MyCustomOutput");
}

TEST_CASE("output_modification_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    o.setOpenPMD("1.0.0");
    REQUIRE(o.openPMD() == "1.0.0");

    o.setOpenPMDextension(1);
    REQUIRE(o.openPMDextension() == static_cast<uint32_t>(1));

    o.setMeshesPath("customMeshesPath");
    REQUIRE(o.meshesPath() == "customMeshesPath/");

    o.setParticlesPath("customParticlesPath");
    REQUIRE(o.particlesPath() == "customParticlesPath/");

    o.setIterationFormat("SomeOtherOutputScheme_%T");
    REQUIRE(o.iterationFormat() == "SomeOtherOutputScheme_%T");

    o.setName("MyOutput");
    REQUIRE(o.name() == "MyOutput");

    o.snapshots().setAttribute("test", "value");
    REQUIRE(o.iterations.getAttribute("test").get<std::string>() == "value");

    o.iterations.setAttribute<int>("test2", 2);
    REQUIRE(o.snapshots().getAttribute("test2").get<int>() == 2);

    o.iterations[0];
}

TEST_CASE("iteration_default_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    Iteration &i = o.iterations[42];

    REQUIRE(i.time<double>() == static_cast<double>(0));
    REQUIRE(i.dt<double>() == static_cast<double>(1));
    REQUIRE(i.timeUnitSI() == static_cast<double>(1));
    REQUIRE(i.numAttributes() == 3);
    REQUIRE(i.meshes.empty());
    REQUIRE(i.particles.empty());
}

TEST_CASE("iteration_modification_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    Iteration &i = o.iterations[42];

    float time = 0.314f;
    i.setTime(time);
    REQUIRE(i.time<float>() == time);

    double dt = 0.42;
    i.setDt(dt);
    REQUIRE(i.dt<long double>() == static_cast<long double>(dt));

    i.setTimeUnitSI(0.000000000001);
    REQUIRE(i.timeUnitSI() == static_cast<double>(0.000000000001));
}

TEST_CASE("particleSpecies_modification_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    auto &particles = o.iterations[42].particles;
    REQUIRE(0 == particles.numAttributes());
    auto &species = particles["species"];
    REQUIRE(1 == particles.size());
    REQUIRE(1 == particles.count("species"));
    REQUIRE(0 == species.numAttributes());
    auto dset = Dataset(Datatype::DOUBLE, {1});
    species["position"][RecordComponent::SCALAR].resetDataset(dset);
    species["positionOffset"][RecordComponent::SCALAR].resetDataset(dset);
    REQUIRE(1 == species.count("positionOffset"));
    auto &patches = species.particlePatches;
    REQUIRE(0 == patches.size());
    REQUIRE(0 == patches.numAttributes());
    auto &offset = patches["offset"];
    REQUIRE(0 == offset.size());
    REQUIRE(1 == offset.numAttributes()); // unitDimension
    std::array<double, 7> zeros{{0., 0., 0., 0., 0., 0., 0.}};
    REQUIRE(zeros == offset.unitDimension());

    auto &off_x = offset["x"];
    off_x.resetDataset(dset);
    REQUIRE(1 == off_x.unitSI());
}

TEST_CASE("record_constructor_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    ParticleSpecies ps = o.iterations[42].particles["species"];
    Record &r = ps["record"];
    auto dset = Dataset(Datatype::DOUBLE, {1});
    ps["position"][RecordComponent::SCALAR].resetDataset(dset);
    ps["positionOffset"][RecordComponent::SCALAR].resetDataset(dset);

    // unitSI is set upon flushing
    // REQUIRE(r["x"].unitSI() == 1);
    REQUIRE(r["x"].resetDataset(dset).numAttributes() == 0); /* unitSI */
    // REQUIRE(r["y"].unitSI() == 1);
    REQUIRE(r["y"].resetDataset(dset).numAttributes() == 0); /* unitSI */
    // REQUIRE(r["z"].unitSI() == 1);
    REQUIRE(r["z"].resetDataset(dset).numAttributes() == 0); /* unitSI */
    std::array<double, 7> zeros{{0., 0., 0., 0., 0., 0., 0.}};
    REQUIRE(r.unitDimension() == zeros);
    REQUIRE(r.timeOffset<float>() == static_cast<float>(0));
    REQUIRE(r.numAttributes() == 2); /* timeOffset, unitDimension */
    o.flush();
    REQUIRE(r["x"].unitSI() == 1);
    REQUIRE(r["y"].unitSI() == 1);
    REQUIRE(r["z"].unitSI() == 1);
}

TEST_CASE("record_modification_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    auto species = o.iterations[42].particles["species"];
    Record &r = species["position"];
    auto dset = Dataset(Datatype::DOUBLE, {1});
    species["position"][RecordComponent::SCALAR].resetDataset(dset);
    species["positionOffset"][RecordComponent::SCALAR].resetDataset(dset);

    using RUD = UnitDimension;
    r.setUnitDimension(
        {{RUD::L, 1.}, {RUD::M, 1.}, {RUD::T, -3.}, {RUD::I, -1.}});
    std::array<double, 7> e_field_unitDimension{{1., 1., -3., -1., 0., 0., 0.}};
    REQUIRE(r.unitDimension() == e_field_unitDimension);

    r.setUnitDimension({{RUD::L, 0.}, {RUD::T, -2.}});
    std::array<double, 7> b_field_unitDimension{{0., 1., -2., -1., 0., 0., 0.}};
    REQUIRE(r.unitDimension() == b_field_unitDimension);

    float timeOffset = 0.314f;
    r.setTimeOffset(timeOffset);
    REQUIRE(r.timeOffset<float>() == timeOffset);
}

TEST_CASE("recordComponent_modification_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    ParticleSpecies ps = o.iterations[42].particles["species"];
    Record &r = ps["record"];
    auto dset = Dataset(Datatype::DOUBLE, {1});
    ps["position"][RecordComponent::SCALAR].resetDataset(dset);
    ps["positionOffset"][RecordComponent::SCALAR].resetDataset(dset);

    r["x"].setUnitSI(2.55999e-7);
    r["y"].setUnitSI(4.42999e-8);
    REQUIRE(
        r["x"].resetDataset(dset).unitSI() == static_cast<double>(2.55999e-7));
    REQUIRE(r["x"].numAttributes() == 1); /* unitSI */
    REQUIRE(
        r["y"].resetDataset(dset).unitSI() == static_cast<double>(4.42999e-8));
    REQUIRE(r["y"].numAttributes() == 1); /* unitSI */

    r["z"].setUnitSI(1);
    REQUIRE(r["z"].resetDataset(dset).unitSI() == static_cast<double>(1));
    REQUIRE(r["z"].numAttributes() == 1); /* unitSI */
}

TEST_CASE("mesh_constructor_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    Mesh &m = o.iterations[42].meshes["E"];

    std::vector<double> pos{0};
    /* unitSI and position are set to default values upon flushing */
    REQUIRE(m["x"].resetDataset(globalDataset).numAttributes() == 0);
    REQUIRE(m["y"].resetDataset(globalDataset).numAttributes() == 0);
    REQUIRE(m["z"].resetDataset(globalDataset).numAttributes() == 0);

    REQUIRE(m.geometry() == Mesh::Geometry::cartesian);
    REQUIRE(m.dataOrder() == Mesh::DataOrder::C);
    std::vector<std::string> al{"x"};
    REQUIRE(m.axisLabels() == al);
    std::vector<double> gs{1};
    REQUIRE(m.gridSpacing<double>() == gs);
    std::vector<double> ggo{0};
    REQUIRE(m.gridGlobalOffset() == ggo);
    REQUIRE(
        m.numAttributes() ==
        7); /* axisLabels, dataOrder, geometry, gridGlobalOffset, gridSpacing,
               timeOffset, unitDimension */

    o.flush();
    REQUIRE(m["x"].unitSI() == 1);
    REQUIRE(m["x"].numAttributes() == 2); /* unitSI, position */
    REQUIRE(m["x"].position<double>() == pos);
    REQUIRE(m["y"].unitSI() == 1);
    REQUIRE(m["y"].numAttributes() == 2); /* unitSI, position */
    REQUIRE(m["y"].position<double>() == pos);
    REQUIRE(m["z"].unitSI() == 1);
    REQUIRE(m["z"].numAttributes() == 2); /* unitSI, position */
    REQUIRE(m["z"].position<double>() == pos);
}

TEST_CASE("mesh_modification_test", "[core]")
{
    Series o = Series("./MyOutput_%T.json", Access::CREATE);

    Mesh &m = o.iterations[42].meshes["E"];
    m["x"].resetDataset(globalDataset);
    m["y"].resetDataset(globalDataset);
    m["z"].resetDataset(globalDataset);

    m.setGeometry(Mesh::Geometry::spherical);
    REQUIRE(m.geometry() == Mesh::Geometry::spherical);
    REQUIRE(m.numAttributes() == 7);
    m.setDataOrder(Mesh::DataOrder::F);
    REQUIRE(m.dataOrder() == Mesh::DataOrder::F);
    REQUIRE(m.numAttributes() == 7);
    std::vector<std::string> al{"z_", "y_", "x_"};
    m.setAxisLabels({"z_", "y_", "x_"});
    REQUIRE(m.axisLabels() == al);
    REQUIRE(m.numAttributes() == 7);
    std::vector<double> gs{1e-5, 2e-5, 3e-5};
    m.setGridSpacing(gs);
    REQUIRE(m.gridSpacing<double>() == gs);
    REQUIRE(m.numAttributes() == 7);
    std::vector<double> ggo{1e-10, 2e-10, 3e-10};
    m.setGridGlobalOffset({1e-10, 2e-10, 3e-10});
    REQUIRE(m.gridGlobalOffset() == ggo);
    REQUIRE(m.numAttributes() == 7);
    m.setGridUnitSI(42.0);
    REQUIRE(m.gridUnitSI() == static_cast<double>(42));
    REQUIRE(m.numAttributes() == 8);
    std::string gp{"FORMULA GOES HERE"};
    m.setGeometryParameters("FORMULA GOES HERE");
    REQUIRE(m.geometryParameters() == gp);
    REQUIRE(m.numAttributes() == 9);

    m["x"].setPosition(std::vector<float>{0, 0, 0});
    REQUIRE(m.numAttributes() == 9);
}

TEST_CASE("structure_test", "[core]")
{
#if openPMD_USE_INVASIVE_TESTS
    Series o = Series("./new_openpmd_output_%T.json", Access::CREATE);

    REQUIRE(o.IOHandler());
    REQUIRE(o.iterations.IOHandler());
    REQUIRE(!o.parent());
    REQUIRE(o.iterations.parent() == getWritable(&o));

    Iteration i = o.iterations[1];
    REQUIRE(i.IOHandler());
    REQUIRE(o.iterations[1].IOHandler());
    REQUIRE(i.parent() == getWritable(&o.iterations));
    REQUIRE(o.iterations[1].parent() == getWritable(&o.iterations));

    Mesh m = o.iterations[1].meshes["M"];
    REQUIRE(m.IOHandler());
    REQUIRE(o.iterations[1].meshes["M"].IOHandler());
    REQUIRE(m.parent() == getWritable(&o.iterations[1].meshes));
    REQUIRE(
        o.iterations[1].meshes["M"].parent() ==
        getWritable(&o.iterations[1].meshes));

    MeshRecordComponent mrc = o.iterations[1].meshes["M"]["MRC"];
    REQUIRE(mrc.IOHandler());
    REQUIRE(o.iterations[1].meshes["M"]["MRC"].IOHandler());
    REQUIRE(mrc.parent() == getWritable(&o.iterations[1].meshes["M"]));
    REQUIRE(
        o.iterations[1].meshes["M"]["MRC"].parent() ==
        getWritable(&o.iterations[1].meshes["M"]));
    mrc = o.iterations[1].meshes["M"]["MRC"].makeConstant(1.0);
    REQUIRE(mrc.IOHandler());
    REQUIRE(o.iterations[1].meshes["M"]["MRC"].IOHandler());
    REQUIRE(mrc.parent() == getWritable(&o.iterations[1].meshes["M"]));
    REQUIRE(
        o.iterations[1].meshes["M"]["MRC"].parent() ==
        getWritable(&o.iterations[1].meshes["M"]));

    MeshRecordComponent scalar_mrc =
        o.iterations[1].meshes["M2"][MeshRecordComponent::SCALAR];
    REQUIRE(scalar_mrc.IOHandler());
    REQUIRE(o.iterations[1].meshes["M2"].IOHandler());
    REQUIRE(
        o.iterations[1].meshes["M2"][MeshRecordComponent::SCALAR].IOHandler());
    REQUIRE(scalar_mrc.parent() == getWritable(&o.iterations[1].meshes));
    REQUIRE(
        o.iterations[1].meshes["M2"].parent() ==
        getWritable(&o.iterations[1].meshes));
    REQUIRE(
        o.iterations[1].meshes["M2"][MeshRecordComponent::SCALAR].parent() ==
        getWritable(&o.iterations[1].meshes));
    scalar_mrc =
        o.iterations[1].meshes["M2"][MeshRecordComponent::SCALAR].makeConstant(
            1.0);
    REQUIRE(scalar_mrc.IOHandler());
    REQUIRE(o.iterations[1].meshes["M2"].IOHandler());
    REQUIRE(
        o.iterations[1].meshes["M2"][MeshRecordComponent::SCALAR].IOHandler());
    REQUIRE(scalar_mrc.parent() == getWritable(&o.iterations[1].meshes));
    REQUIRE(
        o.iterations[1].meshes["M2"].parent() ==
        getWritable(&o.iterations[1].meshes));
    REQUIRE(
        o.iterations[1].meshes["M2"][MeshRecordComponent::SCALAR].parent() ==
        getWritable(&o.iterations[1].meshes));

    ParticleSpecies ps = o.iterations[1].particles["P"];
    REQUIRE(ps.IOHandler());
    REQUIRE(o.iterations[1].particles["P"].IOHandler());
    REQUIRE(ps.parent() == getWritable(&o.iterations[1].particles));
    REQUIRE(
        o.iterations[1].particles["P"].parent() ==
        getWritable(&o.iterations[1].particles));

    REQUIRE(o.iterations[1].particles["P"].particlePatches.IOHandler());
    REQUIRE(
        o.iterations[1].particles["P"].particlePatches.parent() ==
        getWritable(&o.iterations[1].particles["P"]));

    auto dset = Dataset(Datatype::DOUBLE, {1});
    o.iterations[1]
        .particles["P"]["position"][RecordComponent::SCALAR]
        .resetDataset(dset);
    o.iterations[1]
        .particles["P"]["positionOffset"][RecordComponent::SCALAR]
        .resetDataset(dset);

    Record r = o.iterations[1].particles["P"]["PR"];
    REQUIRE(r.IOHandler());
    REQUIRE(o.iterations[1].particles["P"]["PR"].IOHandler());
    REQUIRE(r.parent() == getWritable(&o.iterations[1].particles["P"]));
    REQUIRE(
        o.iterations[1].particles["P"]["PR"].parent() ==
        getWritable(&o.iterations[1].particles["P"]));

    RecordComponent rc = o.iterations[1].particles["P"]["PR"]["PRC"];
    REQUIRE(rc.IOHandler());
    REQUIRE(o.iterations[1].particles["P"]["PR"]["PRC"].IOHandler());
    REQUIRE(rc.parent() == getWritable(&o.iterations[1].particles["P"]["PR"]));
    REQUIRE(
        o.iterations[1].particles["P"]["PR"]["PRC"].parent() ==
        getWritable(&o.iterations[1].particles["P"]["PR"]));
    rc = o.iterations[1].particles["P"]["PR"]["PRC"].makeConstant(1.0);
    REQUIRE(rc.IOHandler());
    REQUIRE(o.iterations[1].particles["P"]["PR"]["PRC"].IOHandler());
    REQUIRE(rc.parent() == getWritable(&o.iterations[1].particles["P"]["PR"]));
    REQUIRE(
        o.iterations[1].particles["P"]["PR"]["PRC"].parent() ==
        getWritable(&o.iterations[1].particles["P"]["PR"]));

    RecordComponent scalar_rc =
        o.iterations[1].particles["P"]["PR2"][RecordComponent::SCALAR];
    REQUIRE(scalar_rc.IOHandler());
    REQUIRE(o.iterations[1]
                .particles["P"]["PR2"][RecordComponent::SCALAR]
                .IOHandler());
    REQUIRE(scalar_rc.parent() == getWritable(&o.iterations[1].particles["P"]));
    REQUIRE(
        o.iterations[1]
            .particles["P"]["PR2"][RecordComponent::SCALAR]
            .parent() == getWritable(&o.iterations[1].particles["P"]));
    scalar_rc = o.iterations[1]
                    .particles["P"]["PR2"][RecordComponent::SCALAR]
                    .makeConstant(1.0);
    REQUIRE(scalar_rc.IOHandler());
    REQUIRE(o.iterations[1]
                .particles["P"]["PR2"][RecordComponent::SCALAR]
                .IOHandler());
    REQUIRE(scalar_rc.parent() == getWritable(&o.iterations[1].particles["P"]));
    REQUIRE(
        o.iterations[1]
            .particles["P"]["PR2"][RecordComponent::SCALAR]
            .parent() == getWritable(&o.iterations[1].particles["P"]));

    REQUIRE(
        0 ==
        o.iterations[1].particles["P"].particlePatches.count("numParticles"));
    REQUIRE(
        0 ==
        o.iterations[1].particles["P"].particlePatches.count(
            "numParticlesOffset"));

    ParticlePatches pp = o.iterations[1].particles["P"].particlePatches;
    REQUIRE(pp.IOHandler());
    REQUIRE(o.iterations[1].particles["P"].particlePatches.IOHandler());
    REQUIRE(pp.parent() == getWritable(&o.iterations[1].particles["P"]));
    REQUIRE(
        o.iterations[1].particles["P"].particlePatches.parent() ==
        getWritable(&o.iterations[1].particles["P"]));

    PatchRecord pr =
        o.iterations[1].particles["P"].particlePatches["numParticles"];
    REQUIRE(pr.IOHandler());
    REQUIRE(o.iterations[1]
                .particles["P"]
                .particlePatches["numParticles"]
                .IOHandler());
    REQUIRE(
        pr.parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches));
    REQUIRE(
        o.iterations[1]
            .particles["P"]
            .particlePatches["numParticles"]
            .parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches));
    pr = o.iterations[1].particles["P"].particlePatches["extent"];
    REQUIRE(pr.IOHandler());
    REQUIRE(
        o.iterations[1].particles["P"].particlePatches["extent"].IOHandler());
    REQUIRE(
        pr.parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches));
    REQUIRE(
        o.iterations[1].particles["P"].particlePatches["extent"].parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches));

    PatchRecordComponent scalar_prc =
        o.iterations[1].particles["P"].particlePatches["numParticles"]
                                                      [RecordComponent::SCALAR];
    REQUIRE(scalar_prc.IOHandler());
    REQUIRE(o.iterations[1]
                .particles["P"]
                .particlePatches["numParticles"][RecordComponent::SCALAR]
                .IOHandler());
    REQUIRE(
        scalar_prc.parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches));
    REQUIRE(
        o.iterations[1]
            .particles["P"]
            .particlePatches["numParticles"][RecordComponent::SCALAR]
            .parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches));

    PatchRecordComponent prc =
        o.iterations[1].particles["P"].particlePatches["extent"]["x"];
    REQUIRE(prc.IOHandler());
    REQUIRE(o.iterations[1]
                .particles["P"]
                .particlePatches["extent"]["x"]
                .IOHandler());
    REQUIRE(
        prc.parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches["extent"]));
    REQUIRE(
        o.iterations[1]
            .particles["P"]
            .particlePatches["extent"]["x"]
            .parent() ==
        getWritable(&o.iterations[1].particles["P"].particlePatches["extent"]));
    prc.resetDataset(dset);
#else
    std::cerr << "Invasive tests not enabled. Hierarchy is not visible.\n";
#endif
}

TEST_CASE("wrapper_test", "[core]")
{
    Series o = Series("./new_openpmd_output_%T.json", Access::CREATE);

    o.setOpenPMDextension(42);
    o.setIterationEncoding(IterationEncoding::fileBased);
    Series copy = o;
    REQUIRE(copy.openPMDextension() == 42);
    REQUIRE(copy.iterationEncoding() == IterationEncoding::fileBased);
    REQUIRE(copy.name() == "new_openpmd_output_%T");
    copy.setOpenPMD("1.1.0");
    copy.setIterationEncoding(IterationEncoding::groupBased);
    copy.setName("other_name");
    REQUIRE(o.openPMD() == "1.1.0");
    REQUIRE(o.iterationEncoding() == IterationEncoding::groupBased);
    REQUIRE(o.name() == "other_name");

    o.iterations[1].meshes["E"]["x"].resetDataset(
        Dataset(Datatype::USHORT, {42}));
    MeshRecordComponent mrc = o.iterations[1].meshes["E"]["x"];
    REQUIRE(mrc.getDatatype() == Datatype::USHORT);
    REQUIRE(mrc.getExtent() == Extent{42});
    mrc.resetDataset(Dataset(Datatype::LONG_DOUBLE, {7}));
    REQUIRE(
        o.iterations[1].meshes["E"]["x"].getDatatype() ==
        Datatype::LONG_DOUBLE);
    REQUIRE(o.iterations[1].meshes["E"]["x"].getExtent() == Extent{7});

    Container<Iteration, uint64_t> its = o.iterations;
    its[1].meshes["E"]["y"].resetDataset(Dataset(Datatype::CHAR, {2}));
    REQUIRE(o.iterations[1].meshes["E"].count("y") == 1);
    REQUIRE(o.iterations[1].meshes["E"]["y"].getDatatype() == Datatype::CHAR);
    REQUIRE(o.iterations[1].meshes["E"]["y"].getExtent() == Extent{2});
    o.iterations[1].meshes["E"]["z"].resetDataset(
        Dataset(Datatype::FLOAT, {1234}));
    REQUIRE(its[1].meshes["E"].count("z") == 1);
    REQUIRE(its[1].meshes["E"]["z"].getDatatype() == Datatype::FLOAT);
    REQUIRE(its[1].meshes["E"]["z"].getExtent() == Extent{1234});

    o.iterations[2];
    REQUIRE(its.count(2) == 1);
    its[3];
    REQUIRE(o.iterations.count(3) == 1);

    double value = 42.;
    o.iterations[4].meshes["E"]["y"].resetDataset(
        Dataset(Datatype::DOUBLE, {1}));
    o.iterations[4].meshes["E"]["y"].makeConstant(value);
    MeshRecordComponent mrc2 = o.iterations[4].meshes["E"]["y"];
    REQUIRE(mrc2.constant());
    double loadData;
    mrc2.loadChunkRaw(&loadData, {0}, {1});
    o.flush();
    REQUIRE(loadData == value);
    // TODO: do we want to be able to make data constant after already writing
    // it once? value = 43.; mrc2.makeConstant(value);
    REQUIRE_THROWS_WITH(
        mrc2.makeConstant(value),
        Catch::Equals(
            "A recordComponent can not (yet) be made constant after "
            "it has been written."));
    std::array<double, 1> moreData = {{112233.}};
    o.iterations[4].meshes["E"]["y"].loadChunkRaw(moreData.data(), {0}, {1});
    o.flush();
    REQUIRE(moreData[0] == value);
    auto all_data = o.iterations[4].meshes["E"]["y"].loadChunk<double>();
    o.flush();
    REQUIRE(all_data.get()[0] == value);
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(o.iterations[4].meshes["E"]["y"].get().m_chunks.empty());
    REQUIRE(mrc2.get().m_chunks.empty());
#endif

    MeshRecordComponent mrc3 = o.iterations[5].meshes["E"]["y"];
    o.iterations[5].meshes["E"]["y"].resetDataset(
        Dataset(Datatype::DOUBLE, {1}));
    int wrongData = 42;
    REQUIRE_THROWS_WITH(
        o.iterations[5].meshes["E"]["y"].storeChunkRaw(&wrongData, {0}, {1}),
        Catch::Equals(
            "Datatypes of chunk data (INT) and record component "
            "(DOUBLE) do not match."));
    std::shared_ptr<double> storeData = std::make_shared<double>(44);
    o.iterations[5].meshes["E"]["y"].storeChunk(storeData, {0}, {1});
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(o.iterations[5].meshes["E"]["y"].get().m_chunks.size() == 1);
    REQUIRE(mrc3.get().m_chunks.size() == 1);
#endif
    o.flush();
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(o.iterations[5].meshes["E"]["y"].get().m_chunks.empty());
    REQUIRE(mrc3.get().m_chunks.empty());
#endif

    o.iterations[6]
        .particles["electrons"]
        .particlePatches["numParticles"][RecordComponent::SCALAR]
        .resetDataset(Dataset(determineDatatype<uint64_t>(), {4}));
    auto dset = Dataset(Datatype::DOUBLE, {1});
    o.iterations[6]
        .particles["electrons"]["position"][RecordComponent::SCALAR]
        .resetDataset(dset);
    o.iterations[6]
        .particles["electrons"]["positionOffset"][RecordComponent::SCALAR]
        .resetDataset(dset);
    ParticlePatches pp = o.iterations[6].particles["electrons"].particlePatches;
    REQUIRE(
        pp["numParticles"][RecordComponent::SCALAR].getDatatype() ==
        determineDatatype<uint64_t>());
    REQUIRE(
        pp["numParticles"][RecordComponent::SCALAR].getExtent() == Extent{4});
    pp["prop"]["x"].resetDataset(Dataset(Datatype::DOUBLE, {7}));
    REQUIRE(
        o.iterations[6]
            .particles["electrons"]
            .particlePatches["prop"]["x"]
            .getDatatype() == Datatype::DOUBLE);
    REQUIRE(
        o.iterations[6]
            .particles["electrons"]
            .particlePatches["prop"]["x"]
            .getExtent() == Extent{7});
    size_t idx = 0;
    uint64_t val = 10;
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(o.iterations[6]
                .particles["electrons"]
                .particlePatches["numParticles"][RecordComponent::SCALAR]
                .get()
                .m_chunks.empty());
    REQUIRE(pp["numParticles"][RecordComponent::SCALAR].get().m_chunks.empty());
#endif
    pp["numParticles"][RecordComponent::SCALAR].store(idx, val);
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(
        o.iterations[6]
            .particles["electrons"]
            .particlePatches["numParticles"][RecordComponent::SCALAR]
            .get()
            .m_chunks.size() == 1);
    REQUIRE(
        pp["numParticles"][RecordComponent::SCALAR].get().m_chunks.size() == 1);
#endif
    std::stringstream u64str;
    u64str << determineDatatype<uint64_t>();
    REQUIRE_THROWS_WITH(
        o.iterations[6]
            .particles["electrons"]
            .particlePatches["numParticles"][RecordComponent::SCALAR]
            .store(idx + 1, 42.),
        Catch::Equals(
            "Datatypes of patch data (DOUBLE) and dataset (" + u64str.str() +
            ") do not match."));
    o.iterations[6]
        .particles["electrons"]
        .particlePatches["numParticles"][RecordComponent::SCALAR]
        .store(idx + 1, val + 1);
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(
        o.iterations[6]
            .particles["electrons"]
            .particlePatches["numParticles"][RecordComponent::SCALAR]
            .get()
            .m_chunks.size() == 2);
    REQUIRE(
        pp["numParticles"][RecordComponent::SCALAR].get().m_chunks.size() == 2);
#endif
    o.flush();
#if openPMD_USE_INVASIVE_TESTS
    REQUIRE(o.iterations[6]
                .particles["electrons"]
                .particlePatches["numParticles"][RecordComponent::SCALAR]
                .get()
                .m_chunks.empty());
    REQUIRE(pp["numParticles"][RecordComponent::SCALAR].get().m_chunks.empty());
#endif
}

TEST_CASE("baserecord_test", "[core]")
{
    Series o("../samples/testBaseRecord.json", Access::CREATE);
    auto E = o.iterations[100].meshes["E"];
    Mesh B = o.iterations[100].meshes["B"];
    Dataset ds(Datatype::INT, {16, 16});
    for (auto const &component : {"x", "y", "z"})
    {
        E[component].makeConstant(5);
        E[component].resetDataset(ds);
    }
    for (auto const &component : {"x", /* "y", */ "z"})
    {
        B[component].makeConstant(5);
        B[component].resetDataset(ds);
    }
    auto inserted = B.insert(B.find("x"), std::pair{"y", E["y"]});
    REQUIRE(inserted->first == "y");
    B.erase(inserted);
}

TEST_CASE("use_count_test", "[core]")
{
    Series o = Series("./new_openpmd_output.json", Access::CREATE);

    MeshRecordComponent mrc = o.iterations[1].meshes["E"]["x"];
    mrc.resetDataset(Dataset(determineDatatype<uint16_t>(), {42}));
    std::shared_ptr<uint16_t> storeData = std::make_shared<uint16_t>(44);
    REQUIRE(storeData.use_count() == 1);
    mrc.storeChunk(storeData, {0}, {1});
    REQUIRE(storeData.use_count() == 2);
    o.flush();
    REQUIRE(storeData.use_count() == 1);

#if openPMD_USE_INVASIVE_TESTS
    PatchRecordComponent pprc =
        o.iterations[6]
            .particles["electrons"]
            .particlePatches["numParticles"][RecordComponent::SCALAR];
    auto dset = Dataset(Datatype::DOUBLE, {1});
    o.iterations[6]
        .particles["electrons"]["position"][RecordComponent::SCALAR]
        .resetDataset(dset);
    o.iterations[6]
        .particles["electrons"]["positionOffset"][RecordComponent::SCALAR]
        .resetDataset(dset);
    pprc.resetDataset(Dataset(determineDatatype<uint64_t>(), {4}));
    pprc.store(0, static_cast<uint64_t>(1));
    REQUIRE(
        std::get<std::shared_ptr<void const>>(
            static_cast<Parameter<Operation::WRITE_DATASET> *>(
                pprc.get().m_chunks.front().parameter.get())
                ->data.m_buffer)
            .use_count() == 1);
#endif
}

TEST_CASE("empty_record_test", "[core]")
{
    Series o = Series("./new_openpmd_output.json", Access::CREATE);

    o.iterations[1].meshes["E"].setComment(
        "No assumption about contained RecordComponents will be made");
    REQUIRE_THROWS_WITH(
        o.flush(),
        Catch::Equals(
            "A Record can not be written without any contained "
            "RecordComponents: E"));
    o.iterations[1].meshes["E"][RecordComponent::SCALAR].resetDataset(
        Dataset(Datatype::DOUBLE, {1}));
    auto B = o.iterations[1].meshes["B"];
    B.resetDataset(Dataset(Datatype::DOUBLE, {1}));
    o.flush();
}

TEST_CASE("zero_extent_component", "[core]")
{
    Series o = Series("./new_openpmd_output.json", Access::CREATE);

    auto E_x = o.iterations[1].meshes["E"]["x"];
    E_x.setComment("Datasets must contain dimensions.");
    // REQUIRE_THROWS_WITH(E_x.resetDataset(Dataset(Datatype::LONG, {})),
    //                    Catch::Equals("Dataset extent must be at least 1D."));
    REQUIRE_THROWS_WITH(
        E_x.makeEmpty<int>(0),
        Catch::Equals("Dataset extent must be at least 1D."));
    E_x.resetDataset(Dataset(Datatype::DOUBLE, {1}));
}

TEST_CASE("no_file_ending", "[core]")
{
    REQUIRE_THROWS_WITH(
        Series("./new_openpmd_output", Access::CREATE),
        Catch::Equals(
            "Unknown file format! Did you specify a file ending? "
            "Specified file name was './new_openpmd_output'."));
    REQUIRE_THROWS_WITH(
        Series("./new_openpmd_output_%T", Access::CREATE),
        Catch::Equals(
            "Unknown file format! Did you specify a file ending? "
            "Specified file name was './new_openpmd_output_%T'."));
    REQUIRE_THROWS_WITH(
        Series("./new_openpmd_output_%05T", Access::CREATE),
        Catch::Equals(
            "Unknown file format! Did you specify a file ending? "
            "Specified file name was './new_openpmd_output_%05T'."));
    {
        Series(
            "../samples/no_extension_specified",
            Access::CREATE,
            R"({"backend": "json"})");
    }
    REQUIRE(auxiliary::file_exists("../samples/no_extension_specified"));
}

TEST_CASE("backend_via_json", "[core]")
{
    std::string encodingVariableBased =
        R"({"backend": "json", "iteration_encoding": "variable_based"})";
    {
        Series series(
            "../samples/optionsViaJson", Access::CREATE, encodingVariableBased);
        series.iterations[0]; // v-based encoding requires at least 1 iteration
        REQUIRE(series.backend() == "JSON");
        REQUIRE(series.iterationEncoding() == IterationEncoding::variableBased);
    }
#if openPMD_HAVE_ADIOS2
    {
        /*
         * JSON backend should be chosen even if ending .bp is given
         * {"backend": "json"} overwrites automatic detection
         */
        Series series(
            "../samples/optionsViaJson.bp",
            Access::CREATE,
            encodingVariableBased);
        series.iterations[0]; // v-based encoding requires at least 1 iteration
        REQUIRE(series.backend() == "JSON");
        REQUIRE(series.iterationEncoding() == IterationEncoding::variableBased);
    }

    {
        /*
         * BP4 engine should be selected even if ending .sst is given
         */
        Series series(
            "../samples/optionsViaJsonOverwritesAutomaticDetectionFile.sst",
            Access::CREATE,
            R"({"adios2": {"engine": {"type": "file"}}})");
    }
    REQUIRE(
        auxiliary::directory_exists(
            "../samples/optionsViaJsonOverwritesAutomaticDetectionFile.sst"));

    {
        /*
         * BP4 engine should be selected even if ending .sst is given
         */
        Series series(
            "../samples/optionsViaJsonOverwritesAutomaticDetectionBp4.sst",
            Access::CREATE,
            R"({"adios2": {"engine": {"type": "bp4"}}})");
    }
    REQUIRE(
        auxiliary::directory_exists(
            "../samples/optionsViaJsonOverwritesAutomaticDetectionBp4.sst"));

#endif
    std::string encodingFileBased =
        R"({"backend": "json", "iteration_encoding": "file_based"})";
    {
        /*
         * File-based iteration encoding can only be chosen if an expansion
         * pattern is detected in the filename.
         */
        REQUIRE_THROWS_AS(
            [&]() {
                Series series(
                    "../samples/optionsViaJson",
                    Access::CREATE,
                    encodingFileBased);
            }(),
            error::WrongAPIUsage);
    }
    {
        /*
         * ... but specifying both the pattern and the option in JSON should
         * work.
         */
        Series series(
            "../samples/optionsViaJson%06T", Access::CREATE, encodingFileBased);
        series.iterations[1456];
    }
    std::string encodingGroupBased =
        R"({"backend": "json", "iteration_encoding": "group_based"})";
    {
        /*
         * ... and if a pattern is detected, but the JSON config says to use
         * an iteration encoding that is not file-based, the pattern should
         * be ignored.
         */
        Series series(
            "../samples/optionsViaJsonPseudoFilebased%T.json",
            Access::CREATE,
            encodingGroupBased);
        REQUIRE(series.iterationEncoding() == IterationEncoding::groupBased);
    }
    REQUIRE(
        auxiliary::file_exists(
            "../samples/optionsViaJsonPseudoFilebased%T.json"));
}

TEST_CASE("wildcard_extension", "[core]")
{
#if openPMD_HAVE_ADIOS2
#if openPMD_HAVE_ADIOS2_BP5
    constexpr char const *const default_file_ending = "bp5";
#else
    constexpr char const *const default_file_ending = "bp4";
#endif
#endif
    auxiliary::remove_directory("../samples/wildcard");
    auto run_test = [current_test = size_t(0)](
                        std::string const &write_config,
                        std::string const &expected_extension) mutable {
        for (auto [write_access, infix] :
             {std::make_pair(Access::CREATE, ""),
              std::make_pair(Access::CREATE, "_%T"),
              std::make_pair(Access::APPEND, ""),
              std::make_pair(Access::APPEND, "_%T")})
        {
            std::string const wildcard_name = "../samples/wildcard/wildcard_" +
                std::to_string(current_test) + std::string(infix) + ".%E";
            std::string const explicit_name = "../samples/wildcard/wildcard_" +
                std::to_string(current_test) + std::string(infix) + '.' +
                expected_extension;
            ++current_test;

            Series series(wildcard_name, write_access, write_config);
            series.setAttribute("test_index", 0);
            series.setAttribute("json_config", write_config);
            series.setAttribute("expected_extension", expected_extension);
            if (*infix)
            {
                series.iterations[0];
            }
            series.close();

            Series read(wildcard_name, Access::READ_ONLY);
            REQUIRE(read.getAttribute("test_index").get<int>() == 0);
            read.close();

            read = Series(explicit_name, Access::READ_ONLY);
            REQUIRE(read.getAttribute("test_index").get<int>() == 0);
            read.close();
        }
    };
#if openPMD_HAVE_ADIOS2
#if openPMD_HAVE_ADIOS2_BP5
    run_test(
        R"({"adios2": {"engine": {"type": "bp5"}}, "backend": "adios2"})",
        "bp5");
#endif
    run_test(
        R"({"adios2": {"engine": {"type": "bp4"}}, "backend": "adios2"})",
        "bp4");
    run_test(R"({"backend": "adios2"})", default_file_ending);
#endif
#if openPMD_HAVE_HDF5
    run_test(R"({"backend": "hdf5"})", "h5");
#endif
    run_test(R"({"backend": "json"})", "json");

    for (auto &name :
         {"../samples/wildcard/colliding.%E",
          "../samples/wildcard/colliding_%T.%E"})
    {
        REQUIRE_THROWS_AS(
            [&name]() {
                Series read_nonexisting(name, Access::READ_ONLY);
                (void)read_nonexisting;
            }(),
            error::ReadError);
        Series write_json(name, Access::CREATE, R"({"backend": "json"})");
        write_json.iterations[0];
        write_json.close();
        REQUIRE_NOTHROW([&name]() {
            Series read_existing(name, Access::READ_ONLY);
            (void)read_existing;
        }());
#if openPMD_HAVE_ADIOS2
        Series write_adios(name, Access::CREATE, R"({"backend": "adios2"})");
        write_adios.iterations[0];
        write_adios.close();
        REQUIRE_THROWS_AS(
            [&name]() {
                Series read_colliding(name, Access::READ_ONLY);
                (void)read_colliding;
            }(),
            error::ReadError);
#endif
    }
}

TEST_CASE("custom_geometries", "[core]")
{
    std::vector<int> sampleData(10, 0);
    {
        Series write("../samples/custom_geometry.json", Access::CREATE);
        auto E = write.iterations[0].meshes["E"];
        E.setAttribute("geometry", "other:customGeometry");
        // gridUnitDimension is technically an openPMD 2.0 addition, but since
        // it's a non-breaking addition, we can also use it in openPMD 1.*
        // files. However, it only really makes sense to use along with per-axis
        // gridUnitSI definitions, which are in fact breaking in comparison to
        // openPMD 1.*.
        E.setGridUnitDimension(
            {{{UnitDimension::theta, 1}},
             {{UnitDimension::M, 1},
              {UnitDimension::L, 1},
              {UnitDimension::T, 2}}});
        auto E_x = E["x"];
        E_x.resetDataset({Datatype::INT, {10}});
        E_x.storeChunk(sampleData, {0}, {10});

        auto B = write.iterations[0].meshes["B"];
        B.setGeometry("customGeometry");
        auto B_x = B["x"];
        B_x.resetDataset({Datatype::INT, {10}});
        B_x.storeChunk(sampleData, {0}, {10});

        auto e_energyDensity = write.iterations[0].meshes["e_energyDensity"];
        e_energyDensity.setGeometry("other:customGeometry");
        auto e_energyDensity_x = e_energyDensity[RecordComponent::SCALAR];
        e_energyDensity_x.resetDataset({Datatype::INT, {10}});
        e_energyDensity_x.storeChunk(sampleData, {0}, {10});

        auto e_chargeDensity = write.iterations[0].meshes["e_chargeDensity"];
        e_chargeDensity.setGeometry(Mesh::Geometry::other);
        auto e_chargeDensity_x = e_chargeDensity[MeshRecordComponent::SCALAR];
        e_chargeDensity_x.resetDataset({Datatype::INT, {10}});
        e_chargeDensity_x.storeChunk(sampleData, {0}, {10});
    }

    {
        Series read("../samples/custom_geometry.json", Access::READ_ONLY);
        auto E = read.iterations[0].meshes["E"];
        auto compare = unit_representations::AsMaps{
            {{UnitDimension::theta, 1}},
            {{UnitDimension::M, 1},
             {UnitDimension::L, 1},
             {UnitDimension::T, 2}}};
        OPENPMD_REQUIRE_GUARD_WINDOWS(
            unit_representations::asMaps(E.gridUnitDimension()) == compare);
        REQUIRE(
            E.getAttribute("geometry").get<std::string>() ==
            "other:customGeometry");
        REQUIRE(E.geometry() == Mesh::Geometry::other);
        REQUIRE(E.geometryString() == "other:customGeometry");

        auto B = read.iterations[0].meshes["B"];
        REQUIRE(
            B.getAttribute("geometry").get<std::string>() ==
            "other:customGeometry");
        REQUIRE(B.geometry() == Mesh::Geometry::other);
        REQUIRE(B.geometryString() == "other:customGeometry");

        auto e_energyDensity = read.iterations[0].meshes["e_energyDensity"];
        REQUIRE(
            e_energyDensity.getAttribute("geometry").get<std::string>() ==
            "other:customGeometry");
        REQUIRE(e_energyDensity.geometry() == Mesh::Geometry::other);
        REQUIRE(e_energyDensity.geometryString() == "other:customGeometry");

        auto e_chargeDensity = read.iterations[0].meshes["e_chargeDensity"];
        REQUIRE(
            e_chargeDensity.getAttribute("geometry").get<std::string>() ==
            "other");
        REQUIRE(e_chargeDensity.geometry() == Mesh::Geometry::other);
        REQUIRE(e_chargeDensity.geometryString() == "other");
    }
}

TEST_CASE("load_chunk_wrong_datatype", "[core]")
{
    {
        Series write("../samples/some_float_value.json", Access::CREATE);
        Dataset ds{Datatype::FLOAT, {10}};
        std::vector<float> sampleData(10, 1234.5);
        auto rc = write.iterations[0].meshes["rho"][RecordComponent::SCALAR];
        rc.resetDataset(ds);
        rc.storeChunk(sampleData, {0}, {10});
        write.flush();
    }
    {
        Series read("../samples/some_float_value.json", Access::READ_ONLY);

        std::string const err_msg =
            "Type conversion during chunk loading not yet implemented! "
            "Data: FLOAT; Load as: DOUBLE";

        REQUIRE_THROWS_WITH(
            read.iterations[0]
                .meshes["rho"][RecordComponent::SCALAR]
                .loadChunk<double>({0}, {10}),
            Catch::Equals(err_msg));
    }
}

TEST_CASE("DoConvert_single_value_to_vector", "[core]")
{
#if openPMD_HAVE_ADIOS2
    {
        Series write("../samples/writeSingleMesh.bp", Access::CREATE);
        auto E_x = write.iterations[0].meshes["E"]["x"];
        E_x.resetDataset({Datatype::INT, {10}});
        E_x.makeConstant(10);
    }
    {
        Series read("../samples/writeSingleMesh.bp", Access::READ_ONLY);
        auto E = read.iterations[0].meshes["E"];
        REQUIRE(E.axisLabels() == std::vector<std::string>{"x"});
    }
#endif
    {
        char val = 'x';
        Attribute attr{val};

        // the following conversions should be possible
        REQUIRE(attr.get<char>() == 'x'); // no conversion
        REQUIRE(attr.get<unsigned char>() == 'x');
        REQUIRE(attr.get<signed char>() == 'x');
        // all the previous ones, but make them single-element vectors now
        REQUIRE(attr.get<std::vector<char>>() == std::vector<char>{'x'});
        REQUIRE(
            attr.get<std::vector<unsigned char>>() ==
            std::vector<unsigned char>{'x'});
        REQUIRE(
            attr.get<std::vector<signed char>>() ==
            std::vector<signed char>{'x'});
    }
    {
        std::array<double, 7> array{{0, 1, 2, 3, 4, 5, 6}};
        Attribute attr{array};

        // the following conversions should be possible
        REQUIRE(attr.get<std::array<double, 7>>() == array);
        // we don't need array-to-array conversions,
        // so array< int, 7 > cannot be loaded here
        REQUIRE(
            attr.get<std::vector<double>>() ==
            std::vector<double>{0, 1, 2, 3, 4, 5, 6});
        REQUIRE(
            attr.get<std::vector<int>>() ==
            std::vector<int>{0, 1, 2, 3, 4, 5, 6});
    }
    {
        std::vector<double> vector{0, 1, 2, 3, 4, 5, 6};
        std::array<double, 7> arraydouble{{0, 1, 2, 3, 4, 5, 6}};
        Attribute attr{vector};
        REQUIRE(attr.get<std::array<double, 7>>() == arraydouble);

        /*
         * The following test no longer works since we pulled the definition for
         * Attribute::get<T>() from public headers into object files. These are
         * instantiated for a selected subset of datatypes (those used in the
         * Datatype enum), hence this test will now result in a link-time error.
         */
#if 0
        std::array<int, 7> arrayint{{0, 1, 2, 3, 4, 5, 6}};
        the following conversions should be possible
        REQUIRE(attr.get<std::array<int, 7>>() == arrayint);
        REQUIRE_THROWS_WITH(
            (attr.get<std::array<int, 8>>()),
            Catch::Equals(
                "getCast: no vector to array conversion possible "
                "(wrong requested array size)."));
#endif
        REQUIRE(
            attr.get<std::vector<double>>() ==
            std::vector<double>{0, 1, 2, 3, 4, 5, 6});
        REQUIRE(
            attr.get<std::vector<int>>() ==
            std::vector<int>{0, 1, 2, 3, 4, 5, 6});
    }
}

TEST_CASE("unavailable_backend", "[core]")
{
#if !openPMD_HAVE_ADIOS2
    {
        auto fail = []() {
            Series(
                "unavailable.bp", Access::CREATE, R"({"backend": "ADIOS2"})");
        };
        REQUIRE_THROWS_WITH(
            fail(),
            "Wrong API usage: openPMD-api built without support for backend "
            "'ADIOS2'.");
    }
#endif
#if !openPMD_HAVE_ADIOS2
    {
        auto fail = []() { Series("unavailable.bp", Access::CREATE); };
        REQUIRE_THROWS_WITH(
            fail(),
            "Wrong API usage: openPMD-api built without support for backend "
            "'ADIOS2'.");
    }
#endif
#if !openPMD_HAVE_HDF5
    {
        auto fail = []() {
            Series("unavailable.h5", Access::CREATE, R"({"backend": "HDF5"})");
        };
        REQUIRE_THROWS_WITH(
            fail(),
            "Wrong API usage: openPMD-api built without support for backend "
            "'HDF5'.");
    }
#endif
}

TEST_CASE("automatic_variable_encoding", "[adios2]")
{
    automatic_variable_encoding::automatic_variable_encoding();
}

TEST_CASE("unique_ptr", "[core]")
{
    auto stdptr = std::make_unique<int>(5);
    UniquePtrWithLambda<int> ptr = std::move(stdptr);
    auto stdptr_with_custom_del =
        std::unique_ptr<int, auxiliary::CustomDelete<int>>{
            new int{5}, auxiliary::CustomDelete<int>{[](int const *del_ptr) {
                delete del_ptr;
            }}};
    UniquePtrWithLambda<int> ptr2 = std::move(stdptr_with_custom_del);

    UniquePtrWithLambda<int[]> arrptr;
    // valgrind can detect mismatched new/delete pairs
    UniquePtrWithLambda<int[]> arrptrFilled{new int[5]{}};
    UniquePtrWithLambda<int[]> arrptrFilledCustom{
        new int[5]{}, [](int const *p) { delete[] p; }};
}

TEST_CASE("scalar_and_vector", "[core]")
{
    {
        Series series("../samples/scalar_and_vector.json", Access::CREATE);
        auto E = series.iterations[0].meshes["E"];
        E["x"].makeEmpty(Datatype::FLOAT, 3);
        REQUIRE_THROWS_AS(
            E.makeEmpty(Datatype::FLOAT, 3), error::WrongAPIUsage);
    }
    {
        Series series("scalar_and_vector.json", Access::CREATE);
        auto E = series.iterations[0].meshes["E"];
        E.makeEmpty(Datatype::FLOAT, 3);
        REQUIRE_THROWS_AS(E["x"], error::WrongAPIUsage);
    }
    {
        Series series("../samples/scalar_and_vector.json", Access::CREATE);
        auto E = series.iterations[0].meshes["E"];
        E["x"].makeEmpty(Datatype::FLOAT, 3);
    }
    {
        Series read("../samples/scalar_and_vector.json", Access::READ_ONLY);
        auto E = read.iterations[0].meshes["E"];
        REQUIRE(E.size() == 1);
        REQUIRE(!E.scalar());
        REQUIRE(E.contains("x"));
    }
    {
        Series series("../samples/scalar_and_vector.json", Access::CREATE);
        auto E = series.iterations[0].meshes["E"];
        E.makeEmpty(Datatype::FLOAT, 3);
    }
    {
        Series read("../samples/scalar_and_vector.json", Access::READ_ONLY);
        auto E = read.iterations[0].meshes["E"];
        REQUIRE(E.size() == 1);
        REQUIRE(E.scalar());
        REQUIRE(!E.contains("x"));
    }
}
